﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Diagnostics;
using System.Windows.Threading;
using System.Timers;
using System.Threading;
using Microsoft.Surface.Presentation.Controls;

namespace Master
{
    /**
     * CarManager keeps track of all cars that have been added to the map. 
     * Since there is only need for 1 CarManager this class has been implemented using the Singleton Patters.
     * Simply call CarManager.getInstance() to obtain the CarManager instance.
     * 
     * @Creator A.J. Janssen
     * @Changes made by:
     *      - T.T. Dusseldorp
     *      - J.J. Burgers
     * */
    public class CarManager
    {
        /*************
         * VARIABLES *
         *************/
        public static readonly CarManager INSTANCE = new CarManager();
        private List<Thread> addCarThreads = new List<Thread>();
        private List<Car> cars;
        private Random random = new Random();

        /****************
         * CONSTRUCTORS *
         ****************/
        private CarManager()
        {
            cars = new List<Car>();        
        }

        /********************
         * PUBLIC FUNCTIONS *
         ********************/
        /**
         * Method to retrieve the (singleton) instance of the CarManager
         * */
        public static CarManager getInstance()
        {
            return INSTANCE;
        }

        /**
         * Method to hookup the window-close vent to the CarManagers stop-method
         * */
        public void hookupSurfaceWindowEvent(SurfaceWindow window)
        {
            window.Closed += stop;
        }

        /**
         * Method to stop all cars and 
         * */
        public void stop(object sender, EventArgs e)
        {
            stopAddCarThreads();
            stopCars();
        }

        /**
         * Method to add a car with the given carDetails on the given spawnpoint
         * */
        public void addCar(SpawnPoint spawnPoint, CarDetails carDetails)
        {
            Point startPosition = new Point(-300, -300);
            Car car = createCar(carDetails, spawnPoint, startPosition);
            positionCarAtSpawnPoint(spawnPoint, startPosition, car);
            car.startDriving();
            car.CarLeftMap += CarLeftMapEvent;
            cars.Add(car);
        }

        /**
         * Method to add a (random) car the given spawnpoint
         * */
        public void addCar(SpawnPoint spawnPoint)
        {
            Point startPosition = new Point(-300, -300);
            Car car = createCar(spawnPoint, startPosition);
            positionCarAtSpawnPoint(spawnPoint, startPosition, car);
            car.startDriving();
            car.CarLeftMap += CarLeftMapEvent;
            cars.Add(car);
        }

        /**
         * Method to add the specified amount of (random) cars at the given spawnpoint
         * */
        public void addCar(SpawnPoint spawnPoint, int numberOfCars)
        {
            Thread t = new Thread(delegate() { doCreateCars(spawnPoint, numberOfCars); });
            addCarThreads.Add(t);
            t.Start();
            
        }

        /*********************
         * PRIVATE FUNCTIONS *
         *********************/
        private void stopCars() //Method to stop all cars
        {
            foreach (Car car in cars)
            {
                car.stopDriving();
                car.CarLeftMap -= CarLeftMapEvent;
                Map.INSTANCE.Children.Remove(car);
            }
            cars = new List<Car>();
        }

        private void stopAddCarThreads() //method to stop all 'addCar'-threads
        {
            foreach (Thread addCarThread in addCarThreads)
                addCarThread.Abort();
            addCarThreads = new List<Thread>();
        }

        private void doCreateCars(SpawnPoint spawnPoint, int numberOfCars) //functionality of the 'addCar'-thread, will create the given amount of cars
        {
            while (numberOfCars > 0)
            {
                Map.INSTANCE.Dispatcher.Invoke(DispatcherPriority.Render, new Action(delegate()
                {
                    Point startPosition = new Point(-300, -300);
                    Car car = createCar(spawnPoint, startPosition);
                    positionCarAtSpawnPoint(spawnPoint, startPosition, car);
                    if (getCarAtPoint(car.getFrontBumperPoint()) != null || getCarAtPoint(car.getRearBumperPoint()) != null)
                    {
                        removeCar(car);
                        return;
                    }
                    car.startDriving();
                    car.CarLeftMap += CarLeftMapEvent;
                    cars.Add(car);
                    numberOfCars--;   
                }));
                Thread.Sleep(1000);
            }
        }

        private Car createCar(CarDetails carDetails, SpawnPoint spawnPoint, Point startPosition) //method to create a new car with the given details at the given spawnpoint
        {
            Car car = CarFactory.createCar(carDetails.getCarType());
            car.Speed = carDetails.getSpeed();
            return createCar(car, spawnPoint, startPosition);
        }

        private Car createCar(SpawnPoint spawnPoint, Point startPosition) //method to create a new car at the given spawnpoint
        {
            Car car = CarFactory.createRandomCar();
            return createCar(car, spawnPoint, startPosition);
        }

        private Car createCar(Car car, SpawnPoint spawnPoint, Point startPosition) //method to add the given car to the map and update its starting position
        {
            Map.INSTANCE.Children.Add(car);
            int orientation = spawnPoint.getLane().Orientation;
            car.Orientation = orientation;
            car.setInitialNewOrientation(orientation);
            car.Position = startPosition;
            Map.INSTANCE.Dispatcher.Invoke(DispatcherPriority.Render, new Action(delegate() { car.updateTrueBounds(Toolkit.getBounds(car)); }));
            return car;
        }

        private void positionCarAtSpawnPoint(SpawnPoint spawnPoint, Point startPosition, Car car) //method to position the given car the given spawnpoint
        {
            Rect bounds = car.getTrueBounds();
            Point currentCenterPoint = new Point((bounds.Left + bounds.Right) / 2, (bounds.Top + bounds.Bottom) / 2);
            Point wantedCenterPoint = spawnPoint.getPoint(car);
            car.Position = new Point(startPosition.X + (wantedCenterPoint.X - currentCenterPoint.X), startPosition.Y + (wantedCenterPoint.Y - currentCenterPoint.Y));
            Map.INSTANCE.Dispatcher.Invoke(DispatcherPriority.Render, new Action(delegate() { car.updateTrueBounds(Toolkit.getBounds(car)); }));
        }

        /**
         * Method to stop and remove a car
         * */
        public void removeCar(Car car)
        {
            car.stopDriving();
            car.CarLeftMap -= CarLeftMapEvent;
            Map.INSTANCE.Children.Remove(car);
            cars.Remove(car);
        }

        /**
         * Method to retrieve whatever car is located at the given point
         * Return the first car found at this position, return null if no cars are located here
         * */
        public Car getCarAtPoint(Point p)
        {
            foreach (Car car in cars)
            {
                Rect bounds = car.getTrueBounds();
                if (p.X < bounds.Left || p.X > bounds.Right ||
                    p.Y < bounds.Top || p.Y > bounds.Bottom)
                    continue;
                return car;
            }
            return null;
        }

        /**
         * Method to retrieve all cars that fall within the specified bounds
         * */
        public List<Car> getCarsAtBounds(Rect bounds)
        {
            List<Car> returnCars = new List<Car>();
            foreach (Car car in cars)
                if(Toolkit.elementIsAtBounds(car, bounds))
                    returnCars.Add(car);
            return returnCars;
        }

        /**
         * Method to handle a 'CarLeftMap' event
         * */
        private void CarLeftMapEvent(object sender, CarLeftMapEventArgs e)
        {
            Car car = (Car)sender;
            NpOutgoing npOutgoing = getNpOutgoing(car);
            Trace.WriteLine("Removed car at point: " + car.Position);
            if (npOutgoing != null)
            {
                if (npOutgoing.Status == NpOutgoingStatus.ENABLED)
                {
                    CarDetails carDetails = new CarDetails(car);
                    int i = npOutgoing.GetHashCode();
                    npOutgoing.getClient().Send(carDetails);
                }
            }
            removeCar(car);
        }

        /**
         * Method to get the NpOutgoing point closest to the location this car has left the Map
         * */
        private NpOutgoing getNpOutgoing(Car car)
        {
            List<NpOutgoing> outgoingList = Map.INSTANCE.getNpOutgoingPoints();
            double smallestDistance = -1; NpOutgoing closestPoint = null;
            foreach (NpOutgoing outgoingPoint in outgoingList)
            {
                double distance = Toolkit.getTotalDistanceBetweenElements(car, outgoingPoint);
                if (closestPoint == null || distance < smallestDistance)
                {
                    smallestDistance = distance;
                    closestPoint = outgoingPoint;
                }
            }
            return closestPoint;
        }
    }
}
